After this long introductory description of properties, methods, and events that are common to most Visual Basic objects, it's time to see the particular features of all of them individually. The most important visible object is undoubtedly the Form object because you can't display any control without a parent Form. Conversely, you can write some moderately useful applications using only forms that have no controls on them. In this section, I'll show a number of examples that are centered on forms' singular features.
You create a new form at design time using the Add Form command from the Project menu or by clicking on the corresponding icon on the standard toolbar. You can create forms from scratch, or you can take advantage of the many form templates provided by Visual Basic 6. If you don't see the dialog box shown in Figure 2-7, invoke the Options command from the Tools menu, click the Environment tab, and select the topmost check box on the right.
Feel free to create new form templates when you need them. A form template doesn't necessarily have to be a complex form with many controls on it. Even an empty form with a group of properties carefully set can save you some precious time. For example, see the Dialog Form template provided by Visual Basic. To produce your custom form templates, you just have to create a form, add any necessary controls and code, and then save it in the \Template\Forms directory. (The complete path of Visual Basic's template directory can be read and modified in the Environment tab of the Options dialog box.)
Figure 2-7. Form templates offered by Visual Basic 6.
After creating a form and resizing it to meet your requirements, you'll probably want to set a few key properties. BorderStyle is one of the properties that largely affects the form's behavior. Its default value is 2-Sizable, which creates a resizable window. To create a nonresizable form, you should set it to 1-Fixed Single or 3-Fixed Dialog: the only difference between the two settings is that the latter can't show Minimize and Maximize buttons. If you're creating a floating, toolboxlike form, you should use the values 4-Fixed Toolwindow or 5-Sizable Toolwindow. Theoretically, you can also use the value 0-None to exclude any type of border and caption, but you'll rarely find a use for such borderless forms.
Next you must decide what should appear on the title bar. Apart from assigning a suitable string to the form's Caption property, you should also decide whether you want the form to support a system menu and a Close button (ControlBox property, default is True) and a Minimize and a Maximize button (MinButton and MaxButton property, respectively). Selecting the right values for these properties is important because you can't change them at run time through code. If you want the form to start in maximized state, you can set the WindowState property to 2-Maximized.
TIP
To create a captionless resizable window, you must set the ControlBox, MinButton, and MaxButton properties to False and the Caption property to an empty string. If you assign a non-empty string to the Caption property at run time, Visual Basic creates the form's title bar on the fly. Assigning it an empty string at run time makes the title bar disappear again. You can't move a captionless form using the mouse as you normally do with other types of windows.
Visual Basic 5 added three important form properties, which are also present in Visual Basic 6. You can have the form appear in the center of the screen by setting its StartupPosition property to the value 2-Center Screen. And you can make your window unmovable by setting the Moveable property to False. You can set these properties only at design time. The third new property is ShowInTaskbar; if you set this property to False, the form isn't shown in the Windows taskbar. Captionless forms appear in the taskbar as "blank" forms, so you might want to set the ShowInTaskbar property to False for such forms.
A few form properties noticeably affect performance. First and foremost, the AutoRedraw property dictates whether the form is backed up by a persistent bitmap so that when it's covered by another form and then uncovered, Visual Basic can quickly restore its contents from the internal bitmap. AutoRedraw's default value is False: setting it to True speeds up refresh operations but also causes a lot of memory to be allocated for the persistent bitmap. To give you an idea of what this means in practice, for a system with 1024-by-768 screen resolution and 256 colors, a persistent bitmap for a resizable form takes 768 KB. On an 800-by-600 pixel, true-color system, the persistent bitmap takes 1406 KB. If you have more forms running at the same time, you can clearly see that you shouldn't set the AutoRedraw property to True, at least not for all the forms. AutoRedraw affects performance in another way: Each time you perform a graphic method (including printing text and drawing figures), Visual Basic creates the output on the hidden persistent bitmap and then copies the bitmap as a whole on the visible area of the form. Needless to say, this is slower than creating the output directly on the form's surface.
TIP
If AutoRedraw is set to True, Visual Basic creates a persistent bitmap as large as the largest possible size for the form, which means the entire screen for resizable windows. Therefore, you can limit the memory overhead caused by the persistent bitmap if you create smaller forms and set their BorderStyle property to 1-Fixed Single or 3-Fixed Dialog.
The ClipControls property also affects performance. If you execute many graphic methods—such as Line, Circle, Point, and Print—you should set this property to False because all your graphic methods are going to execute about twice as fast. When you set this property to False, Visual Basic doesn't have to create a clipping region. However, if your graphic methods do overlap controls on the form, you're going to experience the unpleasant effect shown in Figure 2-8, so be very careful. (Compare this figure with Figure 2-9, which shows the same application with the more appropriate setting of True for the ClipControls property.) If you don't execute graphic methods, you might leave it set to True (the default value) because it won't slow down the application.
Figure 2-8. Nasty effects of the ClipControls setting when graphic methods overlap existing controls.
Figure 2-9. Running the application shown in Figure 2-8 with the more appropriate setting for the ClipControls property.
The HasDC property is new to Visual Basic 6. The default value for this property is True, which causes Visual Basic to create a permanent device context for this form, and this device context exists as long as the form itself is loaded in memory. (A device context is a structure used by Windows for drawing on a window's surface.) If you set this property to False, Visual Basic creates a device context for the form only when strictly needed and discards it as soon as it's not useful anymore. This setting reduces the application's requirements in terms of system resources and can therefore improve its performance on less powerful machines. On the other hand, it adds a little overhead whenever Visual Basic creates and destroys the temporary device context. This happens when Visual Basic fires an event in the program's code.
CAUTION
You can set the HasDC property to False and still run any existing Visual Basic application without any problem. If, however, you use advanced graphic techniques that bypass Visual Basic and write directly onto the form's device context, you shouldn't cache the form's hDC property in a module-level or a global variable, because Visual Basic can destroy and re-create the form's device context between events. Instead, query the hDC property at the beginning of each event procedure.
To understand how Form objects really work, the best approach is having a look at the sequence of events that they raise. This sequence is shown in the illustration below. I'll describe each event in turn.
The first event in the life of any form is the Initialize event. This event fires as soon as you reference the form's name in your code, even before Visual Basic creates the actual window and the controls on its surface. You usually write code in this event to correctly initialize the form's variables:
Public CustomerName As String Public NewCustomer As Boolean Private Sub Form_Initialize() CustomerName = "" ' This isn't really needed. NewCustomer = True ' This is necessary. End Sub |
When a form is initialized, all its module-level variables (CustomerName and NewCustomer, in the preceding example) are assigned their default values. So it isn't strictly necessary to assign a value to a variable if the value is 0 or an empty string. In the code above, for example, there's no need for the assignment to the CustomerName variable, but you might want to leave it there for better readability.
What happens after the Initialize event depends on how you referenced the form in your code. If you referenced only one of its public variables (or, more correctly, its Public properties), as in the following line of code,
frmCustomer.CustomerName = "John Smith" |
nothing else happens and execution flow goes back to the caller. Your code is now able to set the CustomerName variable/property to the new value because in the meantime Visual Basic has created a new instance of the frmCustomer object. On the other hand, if your code had referenced a form's own property or a control on the form itself, Visual Basic can't complete the operation until it actually creates the window and its child controls. When this step is completed, a Load event is fired:
Private Sub Form_Load() ' You can initialize child controls. txtName.Text = CustomerName If NewCustomer Then chkNewCustomer.Value = vbChecked End Sub |
At this point, the form isn't visible yet. This implies that if you execute graphic commands in this event procedure—including a Print command, which is considered a graphic command in Visual Basic—you won't see anything. Likewise, while you can freely read and modify most controls' properties, you should avoid any operation that can't be performed on invisible controls. For example, you can't invoke a SetFocus method to move the focus on a particular control.
Loading a form doesn't necessarily mean that the form is going to become visible. A form becomes visible only if you invoke its Show method or if the form is the application's startup form. You can decide to load a form and keep it hidden until you set some of its properties, as follows:
' The Load method is optional: Visual Basic loads the form ' when you reference the form or one of its controls. Load frmCustomer ' Directly assign a control's property. ' (Not recommended, but this is just an example.) frmCustomer.txtNotes.Text = gloCustomerNotes frmCustomer.Show |
Directly referencing a form's control from outside the form itself, as the previous code example does, is considered a bad programming technique. I will show you how to correctly initialize control properties in Chapter 9.
One instant before the form becomes visible, Visual Basic fires a Resize event. You usually take advantage of this event to rearrange the controls on the form so that they fit the available space in a nice layout. For example, you might want the txtCustomer control to extend to the right border and the multiline txtNotes control to extend to both the right and the bottom border:
Private Sub Form_Resize() txtCustomer.Width = ScaleWidth _ txtCustomer.Left txtNotes.Width = ScaleWidth _ txtNotes.Left txtNotes.Height = ScaleHeight _ txtNotes.Top End Sub |
The Resize event also fires when the user resizes the form manually and when you programmatically alter the form's size.
Soon after the first Resize event comes the Activate event. This event also fires whenever the form becomes the active form in the current application but not if it loses and then regains the focus because the user switched to another application. The Activate event is most useful when you need to update the form's contents with data that might have been modified in another form. When the focus returns to the current form, you refresh its fields:
Private Sub Form_Activate() ' Refresh displayed information from global variables. txtTotalOrders.Text = gloTotalOrders ... End Sub |
Another event might fire before the form becomes fully functional—the Paint event. This event doesn't fire if you set the form's AutoRedraw property to True. In a Paint event procedure, you're expected to redraw the form's contents using graphic methods, such as Print, Line, Circle, Point, Cls, and so on. Here's an example that draws a colorful circular target:
Private Sub Form_Paint() Dim r As Single, initR As Single Dim x As Single, y As Single, qbc As Integer ' Start with a clean surface. Cls ' The center of all circles x = ScaleWidth / 2: y = ScaleHeight / 2 ' Initial radius is the lower of the two values. If x < y Then initR = x Else initR = y FillStyle = vbFSSolid ' Circles are filled. ' Draw circles, from the outside in. For r = initR To 1 Step -(initR / 16) ' Use a different color for each circle. FillColor = QBColor(qbc) qbc = qbc + 1 Circle (x, y), r Next ' Restore regular filling style. FillStyle = vbFSTransparent End Sub |
The Paint event procedure is executed whenever the form needs to be refreshed—for example, when the user closes or moves away a window that partially or totally covered the form. The Paint event also fires when the user resizes the form and uncovers new areas. But it does not fire if the user shrinks the form. To complete the example above, you may want to manually force a Paint event from within the Resize event so that concentric circles are always in the center of the form:
Private Sub Form_Resize() Refresh End Sub |
CAUTION
You might be tempted to force a Paint event by manually calling the Form_Paint procedure. Don't do that! The correct and most efficient way to repaint a window is to execute its Refresh method and let Visual Basic decide the most appropriate moment to do that. Moreover, if you replace the Refresh method with a direct call to the Form_Paint procedure, in some cases the result is that the Paint event procedure is executed twice!
After the very first Paint event—or immediately after the Activate event, if the AutoRedraw property is set to True—the form is finally ready to accept user input. If the form doesn't contain any controls or if none of its controls can receive the input focus, the form itself receives a GotFocus event. You will rarely write code in a form's GotFocus event, though, because you can always use the Activate event instead.
As I mentioned previously, when you switch to another form in your application, the form receives a Deactivate event and another Activate event when it regains the input focus. The same sequence occurs if you temporarily make a form invisible by setting its Visible property to False or by invoking its Hide method.
When the form is about to be unloaded, the form object receives a QueryUnload event. You can learn why a form is unloading by examining the UnloadMode parameter. I have created this code skeleton, which I reuse in my applications as necessary:
Private Sub Form_QueryUnload(Cancel As Integer, _ UnloadMode As Integer) Select Case UnloadMode Case vbFormControlMenu ' = 0 ' Form is being closed by user. Case vbFormCode ' = 1 ' Form is being closed by code. Case vbAppWindows ' = 2 ' The current Windows session is ending. Case vbAppTaskManager ' = 3 ' Task Manager is closing this application. Case vbFormMDIForm ' = 4 ' MDI parent is closing this form. Case vbFormOwner ' = 5 ' The owner form is closing. End Select End Sub |
You can refuse to unload by setting the Cancel parameter to True, as in the following code:
Private Sub Form_QueryUnload(Cancel As Integer, _ UnloadMode As Integer) ' Don't let the user close this form. Select Case UnloadMode Case vbFormControlMenu, vbAppTaskManager Cancel = True End Select End Sub |
If you don't cancel the unload operation, Visual Basic eventually raises the Unload event and gives you a last chance to prevent the closure of the form. In most cases, you take this opportunity to alert the user that data needs to be saved:
' This is a module-level variable. Dim Saved As Boolean Private Sub Form_Unload(Cancel As Integer) If Not Saved Then MsgBox "Please save data first!" Cancel = True End If End Sub |
Unless you canceled the request, Visual Basic destroys all the controls and then unloads the form and releases all the Windows resources allocated at load time when the Unload event procedure exits. Depending on how you invoked the form, it might also fire the form's Terminate event, which is where you put your clean-up code, close your files, and so on. The reasons why this event might fire (or not) are explained in Chapter 9.
CAUTION
When the Terminate event is fired, the form object is already gone, so you shouldn't reference it or its controls in code. If you accidentally do that, no error is raised. Instead, Visual Basic creates another, new instance of the form object, which silently remains hidden in memory, unnecessarily consuming a lot of system resources.
Forms expose a special property, the Controls collection, which contains all the controls that are currently loaded on the form itself. This collection lets you streamline the code in your form modules often and is the key to some programming techniques that would be otherwise impossible. For example, see how simple it is to clear all the TextBox and ComboBox controls in the form with just four lines of code:
On Error Resume Next For i = 0 To Controls.Count - 1 Controls(i).Text = "" Next |
Error handling is necessary here because you must account for all the controls on the form that don't support the Text property. For example, the Controls collection also includes all the menu items on a form, and menu items don't have a Text property. So, you must account for these cases when you iterate over the collection. Here's an alternative way to loop on all the controls in the collection using the generic Control object and a For Each…Next statement:
Dim ctrl As Control On Error Resume Next For Each ctrl In Controls ctrl.Text = "" Next |
Both preceding code snippets work with any number of controls on the form, and they also work if you cut and paste them in another form module. The beauty of the Controls collection is that it makes it possible to create such generic routines, which couldn't be written in any other way. Later in this book, you'll see many other programming techniques based on the Controls collection.
Visual Basic forms live on your computer screen. Even if you plan to use only a portion of the screen estate for your application, you need in many cases to learn more about what's around you. To this end, Visual Basic provides you with a Screen object, a global object that corresponds to the visible desktop.
A form's Left, Top, Width, and Height properties are expressed in twips. Twips are measurement units that can be used for both screen and printer devices. On the printer, 1 inch corresponds to 1,440 twips; on the screen, it depends on the monitor's size and the video card's current resolution. You can find the current size of the screen, in twips, through the Width and Height properties of the Screen object. You can then use the values of these properties; for example, you can move the current form to the bottom right corner of your monitor using this line of code:
Move Screen.Width - Width, Screen.Height - Height |
Although you can't have the Screen object's properties returned in a unit other than twips, you can easily convert these values into pixels using the Screen object's TwipsPerPixelX and TwipsPerPixelY properties:
' Evaluate the screen width and height in pixels. scrWidth = Screen.Width / Screen.TwipsPerPixelX scrHeight = Screen.Height / Screen.TwipsPerPixelY ' Shrink the current form of 10 pixels along the ' x-axis and 20 pixels along the y-axis. Move Left, Top, Width - 10 * Screen.TwipPerPixelX, _ Height - 20 * Screen.TwipsPerPixelY |
The Screen object also lets you enumerate all the character fonts that are available for the screen through its Font and FontCount properties:
' Load the names of all the screen's fonts in a list box. Dim i As Integer For i = 0 To Screen.FontCount - 1 lstFonts.AddItem Screen.Fonts(i) Next |
The only two properties of the Screen object that can be written to are MousePointer and MouseIcon. (I described these properties earlier in this chapter.) You can modify the mouse pointer using the following statement:
Screen.MousePointer = vbHourglass |
A value assigned to this property actually affects only the current application: If you move the mouse cursor to the desktop or over a window belonging to another application, the original mouse cursor is restored. In this sense, therefore, you aren't actually dealing with a screen property. This concept also applies to the remaining Screen properties, ActiveForm and ActiveControl. ActiveForm is a read-only property that returns a reference to the active form in the current application; ActiveControl returns a reference to the control that has the input focus on the active form. You often use these properties together:
' If the current form is frmCustomer, clear the control that has ' the focus. ' On Error is necessary because you can't be sure that it supports ' the Text property, or even that there actually *is* an active ' control. On Error Resume Next If Screen.ActiveForm.Name = "frmCustomer" Then Screen.ActiveControl.Text = "" End If |
A form can be the active form without even having the input focus. If you have switched to another application, the Screen object continues to return a reference to the last form that was current in your application as the active form. Always keep in mind that the Screen object can't see beyond the current application's boundaries. As far as it's concerned, the current application is the only application running on the system. This is a sort of axiom in Win32 programming: No application should know anything about, or affect in any way, other applications running on the system.
In most Visual Basic applications, you don't display text directly on a form's surface. Instead, you usually make use of Label controls or print your messages inside PictureBox controls. But understanding how you can display text in a form can help you in many situations because it reveals how Visual Basic deals with text in general. Moreover, anything I say here about a form's graphic commands and properties also applies to PictureBox controls.
The most important graphic method for showing text is the Print method. This command has been part of the Basic language since its early incarnations and has survived for all these years without any relevant modification, until it found its way into Visual Basic. Because old MS-DOS programs written in Basic heavily relied on this command for all their user interfaces, it was essential for Visual Basic to support it. Since modern Visual Basic programming doesn't rely on this command anymore, however, I'll cover only its basic features.
NOTE
Don't look for the Print method in the Object Browser because you won't find it. The hybrid nature of this command and its contorted syntax—just think of the many separators you can use in a Print statement, including commas, semicolons, and Tab() functions—prevented Microsoft engineers from including it there. Instead, the Print method is directly implemented in the language at run time, at the expense of a more coherent and complete object-oriented implementation of the language as a whole.
You often use the Print method for a quick-and-dirty output on the form's client area. For example, you can show the form's current size and position with this simple code:
Private Sub Form_Resize() Cls ' Resets the printing position to (0,0) Print "Left = " & Left & vbTab & "Top = " & Top Print "Width = " & Width & vbTab & "Height = " & Height End Sub |
TIP
You can use semicolons instead of the & operator and commas instead of the vbTab constant. But if you purposely stick to a standard syntax and stay clear of special features of the Print method, you can easily recycle your arguments and pass them to your custom methods or assign them to the Caption property of a Label. This can save you some time later, when you want to turn a quick-and-dirty prototype into a regular application.
The output from the Print method is affected by the current values of the Font and ForeColor properties, which I described earlier in this chapter. By default, the BackColor property doesn't affect the Print command because the text is usually printed as if it had a transparent background. Most of the time, this situation doesn't cause any problems because you often print over a clean form's surface, and it accounts for better performance because only the text's pixels must be transferred, not the background color. But if you want to print a message and at the same time you want to erase a previous message in the same position, you can do it by setting the FontTransparent property to False. Otherwise, you'll end up with one message on top of another, making both unreadable.
Normally, each Print command resets the x-coordinate of the current graphic position to 0 and advances the y-coordinate so that the next string is displayed immediately below the previous one. You can learn where the next Print command will display its output by querying the form's CurrentX and CurrentY properties. Under normal conditions, the point (0,0) represents the upper left corner in the client area (that is, the portion of the form inside its border and below its title bar). The x-coordinates increase from left to right, and the y-coordinates increase when you go from top to bottom. You can assign a new value to these properties to print anywhere on your form:
' Show a message centered on the form (more or less). CurrentX = ScaleWidth / 2 CurrentY = ScaleHeight / 2 Print "I'm here!" |
This code, however, doesn't really center the message on the form because only the initial printing point is centered while the rest of the string runs toward the right border. To precisely center a message on a screen, you must first determine how wide and tall it is, which you do using the form's TextHeight and TextWidth methods:
msg = "I'm here, in the center of the form." CurrentX = (ScaleWidth _ TextWidth(msg)) / 2 CurrentY = (ScaleHeight - TextHeight(msg)) / 2 Print msg |
You often use the TextWidth and TextHeight methods to see whether a message can fit within a given area. This strategy is especially useful when you print to a form because the Print method doesn't support automatic wrapping for longer lines. To see how you can remedy this deficiency, add the following code to a blank form and then run the program and resize the form at will. Figure 2-10 illustrates how form resizing works.
Figure 2-10. Automatic wrapping for longer lines of text.
' A routine that formats Print output Private Sub Form_Paint() Dim msg As String, pos As Long, spacePos As Long msg = "You often use the TextWidth and TextHeight methods" _ & " to check if a message can fit within a given area. " _ & vbCrLf & " This is especially necessary when you" _ & " print to a form, because the Print method doesn't" _ & " support automatic wrapping for long lines, and you" _ & " need to solve the problem through code." Cls Do While pos < Len(msg) pos = pos + 1 If Mid$(msg, pos, 2) = vbCrLf Then ' A CR-LF pair, print the string so far and reset variables. Print Left$(msg, pos - 1) msg = LTrim$(Mid$(msg, pos + 2)) pos = 0: spacePos = 0 ElseIf Mid$(msg, pos, 1) = " " Then ' A space, remember its position for later. spacePos = pos End If ' Check the message width so far. If TextWidth(Left$(msg, pos)) > ScaleWidth Then ' The message is too long, so let's split it. ' If we just parsed a space, split it there. If spacePos Then pos = spacePos ' Print the message up to the split point. Print Left$(msg, pos - 1) ' Discard printed characters, and reset variables. msg = LTrim$(Mid$(msg, pos)) pos = 0: spacePos = 0 End If Loop ' Print residual characters, if any. If Len(msg) Then Print msg End Sub Private Sub Form_Resize() Refresh End Sub |
The preceding code works with any font you're using. As an exercise, you can build a general routine that accepts any string and any form reference so that you can easily reuse it in your applications.
Another problem that you might need to solve is how to determine the most appropriate font size for a message so that it fits in a given area. Because you can't be sure which sizes are supported by the current font, you usually find the best font size using a For…Next loop. The following simple program creates a digital clock, which you can enlarge and shrink as you like. The actual clock update is performed by a hidden Timer control:
Private Sub Form_Resize() Dim msg As String, size As Integer msg = Time$ For size = 200 To 8 Step -2 Font.Size = size If TextWidth(msg) <= ScaleWidth And _ TextHeight(msg) <= ScaleHeight Then ' We've found a font size that is OK. Exit For End If Next ' Enable the timer. Timer1.Enabled = True Timer1.Interval = 1000 End Sub Private Sub Timer1_Timer() ' Just print the current time using current font settings. Cls Print Time$ End Sub |
Visual Basic supplies developers with several graphic methods. You can draw individual points and lines as well as more complex geometric shapes, such as rectangles, circles, and ellipses. You have complete control over line color, width, and style, and you can even fill your shapes with a solid color or a pattern.
Undoubtedly, the simplest graphic method to use is Cls. It clears the form's surface, fills it with the background color defined by the BackColor property, and then moves the current graphic position to the (0,0) coordinates. If you assign a new value to the BackColor property, Visual Basic clears the background on its own so that you never need to issue a Cls method yourself after you change the form's background color.
A more useful method is PSet, which modifies the color of a single pixel on the form surface. In its basic syntax, you simply need to specify the x and y coordinates. A third, optional argument lets you specify the color of the pixel, if different from the ForeColor value:
ForeColor = vbRed PSet (0, 0) ' A red pixel PSet (10, 0), vbCyan ' A cyan pixel to its right |
Like many other graphic methods, PSet supports relative positioning using the Step keyword; when you use this keyword, the two values within brackets are treated as offsets from the current graphic point (the position on screen that CurrentX and CurrentY currently point to). The point you draw with the PSet method becomes the current graphic position:
' Set a starting position. CurrentX = 1000: CurrentY = 500 ' Draw 10 points aligned horizontally. For i = 1 To 10 PSet Step (8, 0) Next |
The output from the PSet method is also affected by another form's property, DrawWidth. You can set this property to a value greater than 1 (the default) to draw larger points. Note that while all graphic measures are expressed in twips, this value is in pixels. When you use a value greater than 1, the coordinates passed to the PSet method are considered to be the center of the thicker point (which is actually a small circle). Try these few lines of code for a colorful effect:
For i = 1 To 1000 ' Set a random point width. DrawWidth = Rnd * 10 + 1 ' Draw a point at random coordinates and with random color. PSet (Rnd * ScaleWidth, Rnd * ScaleHeight), _ RGB(Rnd * 255, Rnd * 255, Rnd * 255) Next ' Be polite: restore the default value. DrawWidth = 1 |
The PSet method's counterpart is the Point method, which returns the RGB color value of a given pixel. To see this method in action, create a form with a Label1 control on it, draw some graphics on it (the previous code snippet would be perfect), and add the following routine:
Private Sub Form_MouseMove (Button As Integer, _ Shift As Integer, X As Single, Y As Single) Label1.Caption = "(" & X & "," & Y & ") = " _ & Hex$(Point(X, Y)) End Sub |
Run the program, and move the mouse cursor over the colored spots to display their coordinates and color values.
The next graphic method that you might use in your Visual Basic application is the Line method. It's a powerful command, and its syntax variants let you draw straight lines, empty rectangles, and rectangles filled with solid colors. To draw a straight line, you need only to provide the coordinates of starting and ending points, plus an optional color value. (If you omit a color value, the current ForeColor value is used):
' Draw a thick diagonal red "X" across the form. ' The Line method is affected by the current DrawWidth setting. DrawWidth = 5 Line (0, 0) _ (ScaleWidth, ScaleHeight), vbRed Line (ScaleWidth, 0) _ (0, ScaleHeight), vbRed |
Like the PSet method, the Line method supports the Step keyword to specify relative positioning. The Step keyword can be placed in front of either pair of coordinates, so you can freely mix absolute and relative positioning. If you omit the first argument, the line is drawn from the current graphic position:
' Draw a triangle. Line (1000, 2000)- Step (1000, 0) ' Horizontal line Line -Step (0, 1000) ' Vertical line Line -(1000, 2000) ' Close the triangle. |
The output of the Line method is affected by another form's property, DrawStyle. The default value for this property is 0 (vbSolid), but you can also draw dotted lines in a variety of styles, as you can see in Figure 2-11. Table 2-2 summarizes these styles.
Figure 2-11. The effects of various settings of the DrawStyle property.
Table 2-2. Constants for the DrawStyle property.
Constant | Value | Description |
---|---|---|
vbSolid | 0 | Solid (default value) |
vbDash | 1 | Dashed line |
vbDot | 2 | Dotted line |
vbDashDot | 3 | Line with alternating dashes and dots |
vbDashDotDot | 4 | Line with alternating dashes and double dots |
vbInvisible | 5 | Invisible line |
vbInsideSolid | 6 | Inside solid |
Note that the DrawStyle property affects the graphic output only if DrawWidth is set to 1 pixel; in all other cases, the DrawStyle property is ignored and the line is always drawn in solid mode. Adding B as a fourth argument to the Line method allows you to draw rectangles; in this case, the two points are the coordinates of any two opposite corners:
' A blue rectangle, 2000 twips wide and 1000 twips tall Line (500, 500)- Step (2000, 1000), vbBlue, B |
Rectangles drawn in this way are affected by the current settings of the DrawWidth and FillStyle properties.
Finally, you can draw filled rectangles using the BF argument. The capability of creating empty and filled rectangles lets you create interesting effects. For example, you can draw your own 3-D yellow notes floating on your forms using three lines of code:
' A gray rectangle provides the shadow. Line (500, 500)-Step(2000, 1000), RGB(64, 64, 64), BF ' A white rectangle provides the canvas. Line (450, 450)-Step(2000, 1000), vbYellow, BF ' Complete it with a black border. Line (450, 450)-Step(2000, 1000), vbBlack, B |
Even if you can paint filled rectangles using the BF argument, Visual Basic offers more advanced filling capabilities. You can activate a solid filling style by setting the FillStyle property, the results of which you can see in Figure 2-12. Interestingly, Visual Basic offers a separate color property for the color to be used to fill regions, FillColor, which allows you to draw a rectangle's contour with one color and paint its interior with another color in a single operation. Here's how you can take advantage of this feature to recode the previous example with just two Line methods:
Line (500, 500)-Step(2000, 1000), RGB(64, 64, 64), BF FillStyle = vbFSSolid ' We want a filled rectangle. FillColor = vbYellow ' This is the paint color. Line (450, 450)-Step(2000, 1000), vbBlack, B |
Figure 2-12. The eight styles offered by the FillStyle property.
The FillStyle property can be assigned one of several values, as you can see in Table 2-3.
Table 2-3. Constants for the FillStyle property.
Constant | Value | Description |
---|---|---|
vbFSSolid | 0 | Solid filling |
vbFSTransparent | 1 | Transparent (default value) |
vbHorizontalLine | 2 | Horizontal lines |
vbVerticalLine | 3 | Vertical lines |
vbUpwardDiagonal | 4 | Upward diagonal lines |
vbDownwardDiagonal | 5 | Downward diagonal lines |
vbCross | 6 | Vertical and horizontal crossing lines |
vbDiagonalCross | 7 | Diagonal crossing lines |
The last graphic method offered by Visual Basic is Circle, which is also the most complex of the group in that it enables you to draw circles, ellipses, arcs, and even pie slices. Drawing circles is the simplest action you can do with this method because you merely have to specify the circle's center and its radius:
' A circle with a radius of 1000 twips, near the ' upper left corner of the form Circle (1200, 1200), 1000 |
The Circle method is affected by the current values of the DrawWidth, DrawStyle, FillStyle, and FillColor properties, which means that you can draw circles with thicker borders and fill them with a pattern. The circle's border is usually drawn using the current ForeColor value, but you can override it by passing a fourth argument:
' A circle with a 3-pixel wide green border ' filled with yellow solid color DrawWidth = 3 FillStyle = vbFSSolid FillColor = vbYellow Circle (1200, 1200), 1000, vbGreen |
The preceding example draws a perfect circle on any monitor and at any screen resolution because Visual Basic automatically accounts for the different pixel density along the x- and y-axes. To draw an ellipse, you must skip two more optional arguments (which I'll explain later) and append an aspect ratio to the end of the command. The aspect ratio is the number you get when you divide the y-radius by the x-radius of the ellipse. To complicate matters, however, the value that you pass as the third argument of the method is always the larger of the two radii. So if you want to draw an ellipse inside a rectangular area, you must take additional precautions. This reusable routine lets you draw an ellipse using a simplified syntax:
Sub Ellipse(X As Single, Y As Single, RadiusX As Single, _ RadiusY As Single) Dim ratio As Single, radius As Single ratio = RadiusY / RadiusX If ratio < 1 Then radius = RadiusX Else radius = RadiusY End If Circle (X, Y), radius, , , , ratio End Sub |
The Circle method also allows you to draw both circle and ellipse arcs, using two arguments, start and end. (These are the arguments we skipped a moment ago.) The values of these arguments are the starting and ending angles formed by imaginary lines that connect that arc's extreme points with the center of the figure. Such angles are measured in radians, in a counterclockwise direction. For example, you can draw one quadrant of a perfect circle in this way:
Const PI = 3.14159265358979 Circle (ScaleWidth / 2, ScaleHeight / 2), 1500, vbBlack, 0, PI / 2 |
Of course, you can add a ratio argument if you want to draw an ellipse arc. The Circle method can even draw pie slices, that is, arcs that are connected by a radius to the center of the circle or the ellipse. To draw such figures, you must specify a negative value for the start and end arguments. Figure 2-13, which shows a pie chart with an "exploded" portion, was drawn using the following code:
' Draw a pie with an "exploded" portion. ' NOTE that you can't specify a Null "negative" value ' but you can express it as -(PI * 2). Const PI = 3.14159265358979 FillStyle = vbFSSolid FillColor = vbBlue Circle (ScaleWidth / 2 + 200, ScaleHeight / 2 - 200), _ 1500, vbBlack, -(PI * 2), -(PI / 2) FillColor = vbCyan Circle (ScaleWidth / 2, ScaleHeight / 2), _ 1500, vbBlack, -(PI / 2), -(PI * 2) |
Figure 2-13. Drawing pie charts.
As if all the properties and methods you've seen so far weren't enough, you must take into account yet another property of a Form, DrawMode, when you're writing graphic-intensive applications. This property specifies how the figures you're drawing interact with the pixels that are already on the form's surface. By default, all the pixels in your lines, circles, and arcs simply replace whatever is on the form, but this isn't necessarily what you want all the time. In fact, the DrawMode property lets you vary the effects that you get when you blend the pixels coming from the figure being drawn with those already on the form's surface. (Table 2-4 shows you the values that achieve various effects.)
Table 2-4. Constants for the DrawMode property.
Constant | Value | Description | Bit-op (S=Screen, P=Pen) |
---|---|---|---|
vbBlackness | 1 | The screen color is set to all 0s. (The pen color isn't actually used.) | S = 0 |
vbNotMergePen | 2 | The OR operator is applied to the pen color and the screen color, and then the result is inverted (by applying the NOT operator). | S = Not (S Or P) |
vbMaskNotPen | 3 | The pen color is inverted (using the NOT operator), and then the AND operator is applied to the result and the screen color. | S = S And Not P |
vbNotCopyPen | 4 | The pen color is inverted. | S = Not P |
vbMaskPenNot | 5 | The screen color is inverted (using the NOT operator), and then the AND operator is applied to the result and the pen color. | S = Not S And P |
vbInvert | 6 | Invert the screen color. (The pen color isn't actually used.) | S = Not S |
vbXorPen | 7 | The XOR operator is applied to the pen color and the screen color. | S = S Xor P |
vbNotMaskPen | 8 | The AND operator is applied to the pen color and the screen color, and then the result is inverted (using the NOT operator). | S = Not (S And P) |
vbMaskPen | 9 | The AND operator is applied to the pen color and the color on the screen. | S = S And P |
vbNotXorPen | 10 | The XOR operator is applied to the pen color and the screen color, and then the result is inverted (using the NOT operator). | S = Not (S Xor P) |
vbNop | 11 | No operation (actually turns off drawing). | S = S |
vbMergeNotPen | 12 | The pen color is inverted (by using the NOT operator), and then the OR operator is applied to the result and the screen color. | S = S Or Not P |
vbCopyPen | 13 | Draw a pixel in the color specified by the ForeColor property (default). | S = P |
vbMergePenNot | 14 | The screen color is inverted (using the NOT operator), and then the OR operator is applied to the result and the pen color. | S = Not S Or P |
vbMergePen | 15 | The OR operator is applied to the pen color and the screen color. | S = S Or P |
vbWhiteness | 16 | The screen color is set to all 1s. (The pen color isn't actually used.) | S = -1 |
To understand what each drawing mode really does, you should remember that colors are ultimately represented simply by bits, so the operation of combining the pen color and the color already on the form's surface is nothing but a bit-wise operation on 0s and 1s. If you look at Table 2-4 from this perspective, the contents of the rightmost column make more sense, and you can use it to anticipate the results of your graphic commands. For example, if you draw a yellow point (corresponding to hex value &HFFFF) over a cyan background color (&HFFFF00), you can expect the following results:
vbCopyPen | Yellow (Screen color is ignored.) |
vbXorPen | Magenta (&HFF00FF) |
vbMergePen | White (&HFFFFFF) |
vbMaskPen | Green (&H00FF00) |
vbNotMaskPen | Magenta (&HFF00FF) |
Different draw modes can deliver the same result, especially if you're working with solid colors. (See the vbXorPen and vbNotMaskPen examples above.) I can almost hear you asking, "Should we really worry about all these modes?" The answer is no and yes. No, you don't usually need to worry about them if you're writing applications with trivial or no graphic output. Yes, you should at least know what Visual Basic has to offer you when it's time to do some advanced pixel manipulation.
One of the most useful things that you can do with the DrawMode property is rubber banding, the ability to draw and resize new shapes using the mouse without disturbing the underlying graphic. You use rubber banding techniques—without even knowing it—whenever you draw a shape in Microsoft Paint or any Windows paint program. Have you ever wondered what really happens when you drag one of the rectangle's corners using the mouse? Is Microsoft Paint actually erasing the rectangle and redrawing it in another position? How can you implement the same feature in your Visual Basic applications? The answer is much simpler than you might think and is based on the DrawMode property.
The trick is that if you apply the XOR operator twice to a value on the screen and the same pen value, after the second XOR command the original color on the screen is restored. (If you are familiar with bit-wise operations, this shouldn't surprise you; if you aren't, experiment with them until you are convinced that I am telling the truth.) Therefore, all you have to do is set DrawMode to the 7-vbXorPen value; then draw the rectangle (or line, circle, arc, and so on) once to show it and a second time to erase it. When the user eventually releases the mouse cursor, you set the DrawMode property to 13-vbCopyPen and draw the final rectangle on the form's surface. The following program lets you experiment with rubber banding: You can draw empty rectangles (with random line width and color) by dragging the left button and filled rectangles by dragging the right mouse button.
' Form-level variables Dim X1 As Single, X2 As Single Dim Y1 As Single, Y2 As Single ' True if we are dragging a rectangle Dim dragging As Boolean Private Sub Form_Load() ' Rubber-banding works particularly well on a black background. BackColor = vbBlack End Sub Private Sub Form_MouseDown(Button As Integer, Shift As Integer, _ X As Single, Y As Single) If Button And 3 Then dragging = True ' Remember starting coordinates. X1 = X: Y1 = Y: X2 = X: Y2 = Y ' Select a random color and width. ForeColor = RGB(Rnd * 255, Rnd * 255, Rnd * 255) DrawWidth = Rnd * 3 + 1 ' Draw the very first rectangle in Xor mode. DrawMode = vbXorPen Line (X1, Y1)-(X2, Y2), , B If Button = 2 Then ' Filled rectangles FillStyle = vbFSSolid FillColor = ForeColor End If End If End Sub Private Sub Form_MouseMove(Button As Integer, Shift As Integer, _ X As Single, Y As Single) If dragging Then ' Delete old rectangle (repeat the same command in Xor mode). Line (X1, Y1)-(X2, Y2), , B ' Redraw to new coordinates. X2 = X: Y2 = Y Line (X1, Y1)-(X2, Y2), , B End If End Sub Private Sub Form_MouseUp(Button As Integer, Shift As Integer, _ X As Single, Y As Single) If dragging Then dragging = False ' Draw the definitive rectangle. DrawMode = vbCopyPen Line (X1, Y1)-(X, Y), , B FillStyle = vbFSTransparent End If End Sub |
While the twip is the default unit of measurement for Visual Basic when placing and resizing objects on screen, it isn't the only one available. In fact, forms and some other controls that can work as containers—most notably, PictureBox controls—expose a ScaleMode property that can be set either at design time or run time with one of the values displayed in Table 2-5.
Table 2-5. Constants for the ScaleMode property.
Constant | Value | Description |
---|---|---|
vbUser | 0 | User-defined scale mode |
vbTwips | 1 | Twips (1440 twips per logical inch; 567 twips per logical centimeter) |
vbPoints | 2 | Points (72 points per logical inch) |
vbPixels | 3 | Pixels |
vbCharacters | 4 | Characters (horizontal = 120 twips per unit; vertical = 240 twips per unit) |
vbInches | 5 | Inches |
vbMillimeters | 6 | Millimeters |
vbCentimeters | 7 | Centimeters |
vbHimetric | 8 | Himetric (1000 units = 1 centimeter) |
The form object exposes two methods that let you easily convert between different units of measurement; you use the ScaleX method for horizontal measurements and the ScaleY method for vertical measurements. Their syntax is identical: You pass the value to be converted (the source value), a constant from among those in Table 2-5 that specifies the unit used for the source value (the fromscale argument), and another constant that specifies which unit you want to convert it to (the toscale argument). If you omit the fromscale argument, vbHimetric is assumed; if you omit the toscale argument, the current value of the ScaleMode property is assumed:
' How many twips per pixel along the x-axis? Print ScaleX(1, vbPixels, vbTwips) ' Draw a 50x80 pixel rectangle in the upper left corner ' of the form, regardless of the current ScaleMode. Line (0, 0)-(ScaleX(50, vbPixels), ScaleY(80, vbPixels)), _ vbBlack, B |
NOTE
The ScaleX and ScaleY methods offer the same functionality as the Screen object's TwipsPerPixelX and TwipsPerPixelY properties, work for other measurement units, and don't require that you write the actual conversion code. The only time you should continue to use the Screen's properties is when you are writing code in a BAS module (for example, a generic function or procedure) and you don't have any form reference at hand.
The ScaleMode property is closely related to four other properties. ScaleLeft and ScaleTop correspond to the (x,y) values of the upper left pixel in the client area of the form and are usually both set to 0. ScaleWidth and ScaleHeight correspond to the coordinates of the pixel in the bottom right corner in the client area. If you set a different ScaleMode, these two properties immediately reflect the new setting. For example, if you set ScaleMode to 3-vbPixels, you can then query ScaleWidth and ScaleHeight to learn the size of the client area in pixels. Keep in mind that, even if the current ScaleMode is 1-vbTwips, the ScaleWidth and ScaleHeight properties return values that differ from the form's Width and Height properties, respectively, because the latter ones account for the window's borders and title bar, which are outside the client area. If you know the relationship among these quantities, you can derive some useful information about your form:
' Run this code from inside a form module. ' Ensure that ScaleWidth and ScaleHeight return twips. ' (Next line is useless if you are using default settings.) ScaleMode = vbTwips ' Evaluate the border's width in pixels. BorderWidth = (Width _ ScaleWidth) / Screen.TwipsPerPixelX / 2 ' Evaluate the caption's height in pixels. ' (Assumes that the form has no menu bar) CaptionHeight = (Height _ ScaleHeight) / _ Screen.TwipsPerPixelY _ BorderWidth * 2 |
You can assign the ScaleMode property any value that fits your requirements, but the values that are most frequently used are vbTwips and vbPixels. The latter is useful if you want to retrieve the coordinates of child controls in pixels, which is often necessary if you're performing some advanced graphic command that involves Windows API calls.
The vbUser setting is unique in that you don't usually assign it to the ScaleMode property. Instead, you define a custom coordinate system by setting ScaleLeft, ScaleTop, ScaleWidth, and ScaleHeight properties. When you do it, the ScaleMode property is automatically set to 0-vbUser by Visual Basic. You might need to create a custom coordinate system to simplify the code in an application and have Visual Basic perform all the needed conversions on your behalf. The following program plots a function on a form, using a custom coordinate system. (See the results in Figure 2-14.)
Figure 2-14. Plotting a third-degree polynomial function using a custom coordinate system.
' The portion of X-Y plane to be plotted Const XMIN = -5, XMAX = 5, YMIN = -100, YMAX = 100 Const XSTEP = 0.01 Private Sub Form_Resize() ' Set a custom graphic coordinate system so that ' the visible viewport corresponds to constants above. ScaleLeft = XMIN ScaleTop = YMAX ScaleWidth = XMAX - XMIN ScaleHeight = -(YMAX - YMIN) ' Force a Paint event. Refresh End Sub Private Sub Form_Paint() Dim x As Single, y As Single ' Start with a blank canvas. Cls ForeColor = vbBlack ' Explain what is being displayed. CurrentX = ScaleLeft CurrentY = ScaleTop Print "f(x) = x ^ 3 - 2 * x ^ 2 + 10 * x + 5" CurrentX = ScaleLeft Print "X-interval: [" & XMIN & "," & XMAX & "]" CurrentX = ScaleLeft Print "Y-range: [" & YMIN & "," & YMAX & "]" ' Draw x- and y-axes. Line (XMIN, 0)-(XMAX, 0) Line (0, YMIN)-(0, YMAX) ' Plot the math function. ForeColor = vbRed For x = XMIN To XMAX Step XSTEP y = x ^ 3 - 2 * x ^ 2 + 10 * x + 5 PSet (x, y) Next End Sub |
You should pay attention to several notable things about the preceding code:
First, some theory. Most video cards are theoretically able to display 16 million distinct colors. (I say theoretically because we humans can't distinguish most colors from those nearest in the continuum.) Only relatively few true-color video cards are capable, however, of actually showing that many colors in a given instant on the screen, especially at higher screen definitions. (For the sake of simplicity, I omit a description of hi-color cards capable of 65,536 simultaneously displayed colors.) In all other cases, Windows has to resort to palettes.
A palette is a subset of 256 colors among those theoretically supported by the video card. If a video card works in the palette mode, it devotes 1 byte to each pixel on the screen (instead of the 3 bytes that would be necessary to hold 16 million different color values), thus saving a lot of memory and accelerating most graphic operations. Each of the possible 256 values points to another table, where the video card can find the actual RGB value for each color. Each pixel can have a well-defined color, but there can be no more than 256 distinct colors on the screen at a given moment.
Windows reserves for itself 16 colors and leaves the remaining ones available for use by applications. When the foreground application has to display an image, it uses the available palette entries and tries to match the colors in the image. If the image embeds more distinct colors than the number of available colors, the application has to find a decent compromise—for example, by using the same palette entry for two similar colors. In practice, this isn't a big issue, at least compared with what follows. A more serious problem with palettes, in fact, is that when an application has to display multiple images at the same time, it must arbitrate among different sets of colors. Which one has precedence? What happens to the other images?
Until Visual Basic 5, the solution available to Visual Basic developers wasn't actually a solution. Visual Basic 4 and previous versions simply gave the precedence to the image that was first in z-order—in other words, the form or the control that had the focus. All the other images on the screen were displayed using a palette that in most cases didn't fit their set of colors, and the results were often obscene. Starting with Visual Basic 5, we finally have a choice.
The key to this new capability is the form's PaletteMode property, which can be assigned—both at design time and run time—three different values: 0-vbPaletteModeHalftone, 1-vbPaletteModeUseZOrder, and 2-vbPaletteModeCustom. The Halftone palette is a special fixed palette that contains an assortment of "average" colors; it should provide a reasonable rendering for many images and, above all, should allow multiple images with different palettes to peacefully coexist on the form. (This is the default mode for Visual Basic 5 and 6 forms). The ZOrder mode is the only setting available to previous versions of the language. The Form or PictureBox that has the focus affects the palette used by the video card; it will be shown in the best way possible, but all others probably won't be shown to advantage. The third setting, Custom mode, lets you set up a custom palette. To do so, you assign a bitmap—at design time or at run time, using the LoadPicture function—to the Palette property. In this case, the palette associated with the image you provide becomes the palette used by the form.
This concludes the description of all the properties, methods, and events supported by the Form object. Much of this knowledge will be useful in the next chapter too, where I describe the features of all the intrinsic Visual Basic controls.